29. 农产品交易销售趋势与产品结构分析

本章概要

  • 章节角色:受保护平台方法实验;确定性课堂序列和上市公司财报代理都不是农产品订单观测,只计趋势/结构方法成果,不计需求预测完成案例。
  • 学习材料:清洗后的农产品销售日期、品类和销售额。
  • 本章任务:运行 lst-ch29-sales-trend-prepare 至验证过程,构造趋势、产品结构和时间留出预测。
  • 完成后你将得到:日/月/季度趋势、品类贡献、基线与预测误差表。
  • 自我检查:核对聚合总额与时间切分;样本不足或候选不优于基线时 暂不采用模型。
  • 拓展练习:把趋势与结构分析方法应用到另一中国农产品市场。

本章分析目标

本章以农产品订单字段开展两个维度的分析;其中预测验证使用确定性课堂序列(非市场观测),不构成福建或真实农产品市场依据:

  1. 销售趋势分析:时间规律、季节性、周期性
  2. 产品结构分析:市场格局、价格段、季节性差异

数据准备:导入库与读取数据

Listing 1: 导入分析所需的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# 优先使用课程规定且已安装的思源宋体显示中文
plt.rcParams['font.sans-serif'] = [
    'Source Han Serif SC'
]
plt.rcParams['axes.unicode_minus'] = False

数据准备:产品分类映射

将具体产品名映射到大类,便于后续聚合分析:

Listing 2: 产品分类映射字典
# 产品分类映射字典
category_mapping = {
    '安溪铁观音': '茶叶',
    '武夷岩茶': '茶叶',
    '福州茉莉花': '茶叶',
    '古田银耳': '食用菌',
    '建宁莲子': '中药材',
    '琯溪蜜柚': '水果',
    '宁德大黄鱼': '水产品'
}

销售趋势分析概述

销售趋势分析流程 展示销售趋势分析的四个核心步骤:每日趋势、月度汇总、季度品类、线性预测。 每日趋势 订单数 & 销售额 7日移动平均 月度汇总 柱状图展示 数据标签标注 季度品类 堆叠柱状图 品类结构对比 线性预测 线性趋势拟合 检查要求+未来4期 销售趋势分析四步法 从日度粒度到季度粒度,逐步洞察销售规律

每日销售趋势:核心思路

分析每日订单数与销售额的变化规律:

  • 按日期聚合groupby('下单日期') 统计每日订单数和销售额
  • 7日移动平均rolling(window=7) 平滑短期波动
  • 双子图展示:订单量趋势 + 销售额趋势(真实表参考实现;课堂演示为单幅序列图)

每日销售趋势:关键代码解析

课堂序列演示7期移动平均;真实表先按日聚合再平滑。

展开课堂序列趋势代码
period_index = np.arange(24)  # 构造24个课堂交易日索引
class_sales = 100 + 3 * period_index + np.tile([0, 1, -1, 2, -2, 0], 4)  # 确定性非市场课堂序列
rolling_mean_7 = pd.Series(class_sales).rolling(window=7, min_periods=1).mean()  # 7期移动平均平滑波动
figure, axis = plt.subplots(figsize=(7.0, 3.4))  # 创建适配幻灯片的画布
axis.plot(period_index, class_sales, alpha=0.5, linewidth=1.2, color='#6E7F82', label='课堂序列')  # 课程灰绘制原序列
axis.plot(period_index, rolling_mean_7, linewidth=2.2, color='#EC232A', label='7期移动平均')  # 绘制平滑线
axis.set_xlabel('课堂交易日')  # 设置横轴标签
axis.set_ylabel('销售额(课堂单位)')  # 设置纵轴标签
axis.legend()  # 显示图例
axis.grid(True, alpha=0.3)  # 显示浅网格
figure.tight_layout()  # 收紧边距
plt.show()  # 显示图形
图中展示课堂序列趋势与7期移动平均(确定性非市场观测);读者应依据坐标、图例与注释比较主要模式。
Figure 1: 课堂序列趋势与7期移动平均(确定性非市场观测)

月度销售数据:柱状图展示

将日度数据汇总到月度,便于观察整体变化:

展开课堂月度汇总代码
month_label = np.repeat(['第1月', '第2月', '第3月', '第4月'], 6)  # 每6个课堂交易日合并为一个月
monthly_total = pd.Series(class_sales).groupby(month_label).sum()  # 按月聚合求和
figure, axis = plt.subplots(figsize=(7.0, 3.4))  # 创建适配幻灯片的画布
bars = axis.bar(monthly_total.index, monthly_total.values, color='#00A4E6', alpha=0.85)  # 绘制月度柱状图
for bar_handle in bars:  # 逐柱添加数值标签
    axis.text(bar_handle.get_x() + bar_handle.get_width() / 2, bar_handle.get_height(), f'{bar_handle.get_height():.0f}', ha='center', va='bottom', fontsize=16)  # 柱顶标注合计值
axis.set_xlabel('课堂月份')  # 设置横轴标签
axis.set_ylabel('月度销售额(课堂单位)')  # 设置纵轴标签
axis.grid(True, axis='y', alpha=0.3)  # 显示横向浅网格
figure.tight_layout()  # 收紧边距
plt.show()  # 显示图形
图中展示课堂月度合计柱状图;真实表用 strftime 提取年月后聚合;读者应依据坐标、图例与注释比较主要模式。
Figure 2: 课堂月度合计柱状图;真实表用 strftime 提取年月后聚合

季度品类销售额:堆叠柱状图

年-季度产品大类进行交叉统计(课堂用两个确定性品类演示):

展开课堂季度品类代码
quarter_label = np.repeat(['Q1', 'Q2', 'Q3', 'Q4'], 6)  # 每6个课堂交易日合并为一个季度
tea_share = 0.6 + 0.05 * np.sin(np.arange(24))  # 品类A(茶叶)的确定性占比路径
category_tea = pd.Series(class_sales * tea_share).groupby(quarter_label).sum()  # 茶叶季度合计
category_other = pd.Series(class_sales * (1 - tea_share)).groupby(quarter_label).sum()  # 其他品类季度合计
figure, axis = plt.subplots(figsize=(7.0, 3.4))  # 创建适配幻灯片的画布
axis.bar(category_tea.index, category_tea.values, label='茶叶', color='#00A4E6', alpha=0.85)  # 底层柱
axis.bar(category_other.index, category_other.values, bottom=category_tea.values, label='其他品类', color='#007A86', alpha=0.85)  # 堆叠柱
axis.set_xlabel('课堂季度')  # 设置横轴标签
axis.set_ylabel('季度销售额(课堂单位)')  # 设置纵轴标签
axis.legend()  # 显示图例
axis.grid(True, axis='y', alpha=0.3)  # 显示横向浅网格
figure.tight_layout()  # 收紧边距
plt.show()  # 显示图形
图中展示课堂季度品类堆叠柱状图;真实表 pivot 转宽表后绘制;读者应依据坐标、图例与注释比较主要模式。
Figure 3: 课堂季度品类堆叠柱状图;真实表 pivot 转宽表后绘制

销售额预测:线性回归思路

用线性回归拟合趋势,并按可验证流程给出经过检验的预测:

  1. 特征工程:把日期转为期数索引(课堂为第0—23期)
  2. 时间留出:前18期训练、后6期留出,同一时间段比较末值朴素基线
  3. 判断条件判定:MAE优于基线且≤2.0、误差带半宽≤3.0,任一不满足即暂停
  4. 可视化:历史+训练期趋势+未来4期点预测+回测误差带;灰色仅为未来预测时段(不是不确定性区间)

销售额预测:关键代码

预先声明判断条件:模型须同时满足 MAE < 末值朴素基线MAE ≤ 2.0、回测最大绝对误差带半宽 ≤ 3.0;任一不满足就暂停库存/营销试点。

Listing 3
展开课堂验证代码(25行)
import numpy as np  # 导入数值计算库,保证本块可独立运行
period = np.arange(24)  # 构造连续24期的课堂时间索引
class_sales = 100 + 3 * period + np.tile([0, 1, -1, 2, -2, 0], 4)  # 生成确定性非市场课堂序列
cutoff = 18  # 预先指定前18期训练、后6期按时间留出
x_train, x_holdout = period[:cutoff], period[cutoff:]  # 按时间切分训练期与留出期索引
y_train = class_sales[:cutoff]  # 提取训练期同口径目标
y_holdout = class_sales[cutoff:]  # 提取留出期同口径目标
trend_coef = np.polyfit(x_train, y_train, 1)  # 仅用训练期拟合线性趋势模型
model_holdout = np.polyval(trend_coef, x_holdout)  # 生成留出期模型预测
naive_holdout = np.repeat(y_train[-1], len(y_holdout))  # 构造末值朴素基线
model_mae = np.mean(np.abs(y_holdout - model_holdout))  # 计算模型留出期平均绝对误差
naive_mae = np.mean(np.abs(y_holdout - naive_holdout))  # 计算基线同口径平均绝对误差
band_half = np.max(np.abs(y_holdout - model_holdout))  # 用留出残差定义回测最大误差带半宽
mae_limit, band_limit = 2.0, 3.0  # 预先声明误差与带宽边界
conditions_met = model_mae < naive_mae and model_mae <= mae_limit and band_half <= band_limit  # 执行验证检查要求
full_coef = np.polyfit(period, class_sales, 1)  # 检查要求评估后用全部课堂历史重估趋势
future_period = np.arange(24, 28)  # 定义未来4期预测时段
future_point = np.polyval(full_coef, future_period)  # 计算未来4期点预测
future_low = future_point - band_half  # 生成由回测残差支持的误差带下界
future_high = future_point + band_half  # 生成由回测残差支持的误差带上界
check_text = "符合要求" if conditions_met else "需要继续检查"  # 生成检查结果文本
decision = "允许小规模库存/营销课堂试点" if conditions_met else "暂停库存/营销建议"  # 相应建议
print(f"回测MAE:模型{model_mae:.2f}|末值基线{naive_mae:.2f}|误差带半宽{band_half:.2f}")  # 显示同口径误差
print(f"未来4期:{np.round(future_point, 1)};回测误差带:{np.round(future_low, 1)}{np.round(future_high, 1)}")  # 显示预测摘要
print(f"检查结果:{check_text}(MAE≤{mae_limit:.1f}、半宽≤{band_limit:.1f}、优于基线)|{decision}")  # 显示检查结果与建议
回测MAE:模型1.07|末值基线10.50|误差带半宽2.23
未来4期:[171.9 174.9 177.8 180.8];回测误差带:[169.6 172.6 175.6 178.6]—[174.1 177.1 180.1 183.1]
检查结果:符合要求(MAE≤2.0、半宽≤3.0、优于基线)|允许小规模库存/营销课堂试点

销售额预测:图形输出

展开预测图形代码
figure, axis = plt.subplots(figsize=(7.0, 3.4))  # 创建适配幻灯片的画布
axis.axvspan(23.5, 27.5, color='gray', alpha=0.15, label='未来预测时段')  # 灰色仅标示预测时段,不是区间
axis.axvline(17.5, color='#6E7F82', linewidth=1.5, linestyle=':')  # 标出训练/留出分界(cutoff=18)
axis.text(8.6, 178, '训练', color='#6E7F82', fontsize=16)  # 标注训练期
axis.text(19.4, 178, '留出', color='#6E7F82', fontsize=16)  # 标注留出期
axis.plot(period, class_sales, 'o-', markersize=3, alpha=0.6, color='#6E7F82', label='课堂历史')  # 课程灰绘制历史序列
fit_line = np.polyval(trend_coef, period)  # 训练期拟合的线性趋势
axis.plot(period, fit_line, '--', linewidth=1.6, color='#00A4E6', label='训练期趋势')  # 课程蓝绘制趋势线
axis.plot(future_period, future_point, 's-', color='#EC232A', label='未来4期点预测')  # 课程红绘制未来点预测
axis.fill_between(future_period, future_low, future_high, color='#EC232A', alpha=0.35, linewidth=0, label='回测误差带')  # 提高可见性的误差带
axis.plot(future_period, future_high, color='#EC232A', linewidth=0.8, alpha=0.7)  # 误差带上界线
axis.plot(future_period, future_low, color='#EC232A', linewidth=0.8, alpha=0.7)  # 误差带下界线
axis.set_xlabel('课堂期数')  # 设置横轴标签
axis.set_ylabel('销售额(课堂单位)')  # 设置纵轴标签
axis.legend(loc='upper left', fontsize=16)  # 显示图例
axis.grid(True, alpha=0.3)  # 显示浅网格
figure.tight_layout()  # 收紧边距
plt.show()  # 显示图形
图中展示课堂序列趋势拟合与未来4期预测(灰区为预测时段,阴影为回测误差带);读者应依据坐标、图例与注释比较主要模式。
Figure 4: 课堂序列趋势拟合与未来4期预测(灰区为预测时段,阴影为回测误差带)

销售趋势分析完整代码

以下长函数是需传入真实订单表的扩展实现,本章未调用它,也不把它当作完整的可运行预测结果;其中灰色 axvspan 仅标示未来预测时段,不是预测区间。可运行的课堂依据以刚才的时间留出、基线比较、回测误差带和判断条件输出为准。

Listing 4
展开数据准备函数
from datetime import timedelta  # 导入未来日期偏移工具
import pandas as pd  # 导入订单表处理工具
import matplotlib.pyplot as plt  # 导入趋势图工具
from sklearn.linear_model import LinearRegression  # 导入线性基线模型
def prepare_sales_windows(orders):  # 构造日、月与季度同源汇总
    required={'order_date','order_id','sales_amount','category'}  # 定义订单字段要求
    if not required<=set(orders.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列即停止
    prepared=orders.copy()  # 保留原始输入不被就地修改
    prepared['order_date']=pd.to_datetime(prepared['order_date'],errors='coerce')  # 解析订单日期
    if prepared[list(required)].isna().any().any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 必需值缺失即停止
    daily=prepared.groupby('order_date').agg(order_count=('order_id','count'),sales=('sales_amount','sum')).sort_index()  # 汇总每日订单与销售额
    daily['orders_ma7']=daily['order_count'].rolling(7,min_periods=1).mean()  # 计算订单量七日均值
    daily['sales_ma7']=daily['sales'].rolling(7,min_periods=1).mean()  # 计算销售额七日均值
    monthly=prepared.set_index('order_date').resample('ME').agg(order_count=('order_id','count'),sales=('sales_amount','sum'))  # 汇总月度指标
    quarterly=prepared.assign(quarter=prepared['order_date'].dt.to_period('Q').astype(str)).pivot_table(index='quarter',columns='category',values='sales_amount',aggfunc='sum',fill_value=0)  # 汇总季度品类销售额
    return prepared,daily,monthly,quarterly  # 返回同一订单快照的三种时间粒度
Listing 5
展开历史趋势绘图函数
def plot_sales_history(daily,monthly):  # 绘制日趋势与月度聚合
    figure,axes=plt.subplots(2,1,figsize=(9,5.6),sharex=False)  # 创建上下两个投影子图
    axes[0].plot(daily.index,daily['sales'],alpha=.35,label='每日销售额')  # 绘制每日销售额
    axes[0].plot(daily.index,daily['sales_ma7'],label='七日均值')  # 叠加七日平滑线
    axes[0].set_ylabel('销售额')  # 标注日趋势纵轴
    axes[0].legend()  # 展示日趋势图例
    axes[0].grid(alpha=.3)  # 添加浅色辅助网格
    axes[1].bar(monthly.index.astype(str),monthly['sales'],color='#00A4E6')  # 绘制月度销售额
    axes[1].set_xlabel('月份')  # 标注月度横轴
    axes[1].set_ylabel('销售额')  # 标注月度纵轴
    axes[1].tick_params(axis='x',rotation=45)  # 旋转月份标签避免重叠
    axes[1].grid(axis='y',alpha=.3)  # 添加月度水平网格
    figure.tight_layout()  # 收紧图形边距
    return figure  # 返回可由调用方保存的图形对象
Listing 6
展开留出验证函数
def validate_sales_forecast(daily,holdout_days=30):  # 用时间留出评价线性基线
    if len(daily)<=holdout_days+30: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 历史不足即停止
    time_index=(daily.index-daily.index.min()).days.to_numpy().reshape(-1,1)  # 将日期转换为有序天数特征
    cutoff=len(daily)-holdout_days  # 固定最后三十日为留出集
    model=LinearRegression().fit(time_index[:cutoff],daily['sales'].iloc[:cutoff])  # 只用训练期拟合模型
    holdout_prediction=model.predict(time_index[cutoff:])  # 生成留出期预测
    naive_prediction=daily['sales'].iloc[cutoff-1]  # 构造末值常数基线
    model_mae=float((daily['sales'].iloc[cutoff:].to_numpy()-holdout_prediction).__abs__().mean())  # 计算模型留出MAE
    naive_mae=float((daily['sales'].iloc[cutoff:]-naive_prediction).abs().mean())  # 计算常数基线MAE
    conditions_met=model_mae<naive_mae  # 仅优于朴素基线时通过课堂检查要求
    audit={'cutoff':str(daily.index[cutoff-1].date()),'holdout_days':holdout_days,'model_mae':model_mae,'naive_mae':naive_mae,'conditions_met':conditions_met}  # 记录时间切分与误差比较
    return model,audit  # 返回模型和便于核对检查要求
Listing 7
展开未来情景函数
def forecast_sales_scenario(daily,model,audit,horizon=30):  # 在检查要求通过后生成未来点预测
    if not audit['conditions_met']: return pd.DataFrame(),{'status':'暂不采用模型','reason':'未优于末值基线'}  # 结果不符合要求时不采用预测
    last_date=daily.index.max()  # 取得历史样本末日
    future_dates=pd.date_range(last_date+timedelta(days=1),periods=horizon,freq='D')  # 构造未来日期索引
    future_index=(future_dates-daily.index.min()).days.to_numpy().reshape(-1,1)  # 按训练期起点转换未来特征
    scenario=pd.DataFrame({'point_forecast':model.predict(future_index)},index=future_dates)  # 生成仅供情景讨论的点预测
    return scenario,{'status':'METHOD_FIXTURE_ONLY','horizon':horizon,'not_interval':True}  # 明确不是置信或预测区间

产品结构分析概述

产品结构分析五个维度 展示产品结构分析涵盖的五个核心维度:品类统计、子类Top3、价格段分析、树形图、季节性分析。 产品结构 分析 品类统计 大类销售额排名 子类 Top3 细分产品排名 价格段分析 订单/销售额占比 销售额树形图 季节性分析 热力图占比

品类统计:按大类聚合销售数据

展开完整代码(投影默认折叠)
# 按商品大类统计
category_stats = data.groupby('category').agg({
    'order_id': 'count',
    'sales_amount': 'sum'
}).reset_index()
category_stats.columns = ['产品类型', '订单数', '销售额']

# 计算平均订单金额
category_stats['平均订单金额'] = (
    category_stats['销售额'] / category_stats['订单数']
)
category_stats = category_stats.sort_values('销售额', ascending=False)
  • agg() 同时计算多个聚合指标
  • 按销售额降序排列,快速定位核心品类

商品定价分析:价格段划分

使用 pd.cut() 将连续价格离散化为价格段:

展开完整代码(投影默认折叠)
# 将价格划分为三个区间
data['价格段'] = pd.cut(
    data['price'],
    bins=[0, 100, 200, 500],
    labels=['0-100元', '100-200元', '200-500元']
)

# 按价格段统计订单数和销售额
price_range_stats = data.groupby('价格段').agg({
    'order_id': 'count',
    'sales_amount': 'sum'
}).reset_index()
  • bins 定义分割点,labels 定义区间标签
  • 分析不同价格段的订单占比与销售额占比

树形图可视化:squarify 库

使用 squarify 绘制矩形树形图,直观展示各品类占比:

展开完整代码(投影默认折叠)
import squarify

category_sales['标签'] = (
    category_sales['产品类型'] + '\n'
    + category_sales['销售额'].apply(lambda x: f'{x:,.0f}元')
)

squarify.plot(
    sizes=category_sales['销售额'],
    label=category_sales['标签'],
    alpha=0.8,
    color=sns.color_palette('viridis', len(category_sales))
)
  • 矩形面积与销售额成正比
  • 一眼识别主力品类和长尾品类

季节性销售分析:热力图

分析不同产品在四个季节的销售占比:

展开完整代码(投影默认折叠)
# 按季节和产品类型统计
seasonal_product = data.groupby(
    ['order_season', 'category']
)['sales_amount'].sum().reset_index()

# 透视 + 百分比计算
season_pivot = seasonal_product.pivot(
    index='category', columns='order_season',
    values='sales_amount'
)
season_pct = season_pivot.div(
    season_pivot.sum(axis=1), axis=0
) * 100
  • div(..., axis=0) 按行归一化
  • sns.heatmap() 以热力颜色直观展示差异

运行前预测|平台任务解答代码

  • 输入预测:运行前先写出 datacategory_mappingseasonscategory_stats 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到“产品类型统计:”相关结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:说明平台填空题的思路并按平台要求录入。受保护代码中的缩进按平台原文保留;本地学习时,应另用语法正确的代码理解计算过程。

⭐ 平台任务解答代码

展开完整代码(投影默认折叠)
# 注:processed_data.csv数据文件本地没有,但平台已经内置;squarify包本地未安装,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import seaborn as sns  # 导入Seaborn可视化库
import warnings  # 导入warnings模块用于控制警告输出
warnings.filterwarnings('ignore')  # 忽略警告
plt.rcParams['font.sans-serif'] = ['SimHei']  # 中文显示
plt.rcParams['axes.unicode_minus'] = False  # 负号显示
data = pd.read_csv('processed_data.csv')  # 读取数据

# 产品分类映射
category_mapping = {'安溪铁观音':'茶叶','武夷岩茶':'茶叶','福州茉莉花':'茶叶','古田银耳':'食用菌','建宁莲子':'中药材','琯溪蜜柚':'水果','宁德大黄鱼':'水产品'}
data['category'] = data['product_name'].map(category_mapping).fillna(data['category'])  # 更新产品分类
print(data.head())  # 查看数据

# 产品分析
import squarify  # 导入树形图库
# 创建季节特征(复用之前的季节映射)
seasons = {1: '冬季', 2: '冬季', 3: '春季', 4: '春季', 5: '春季', 6: '夏季', 7: '夏季', 8: '夏季', 9: '秋季', 10: '秋季', 11: '秋季', 12: '冬季'}
data['下单月份'] = pd.to_datetime(data['order_date']).dt.month  # 提取月份
data['order_season'] = data['下单月份'].map(seasons)  # 映射季节

def product_analysis(orders):  # 产品分析函数
    # 按商品大类统计销售数据
    category_stats = data.groupby('category').agg({'order_id':'count','sales_amount':'sum'}).reset_index()  #【要求1】
    category_stats.columns = ['产品类型', '订单数', '销售额']  # 重命名列
    category_stats['平均订单金额'] = category_stats['销售额'] / category_stats['订单数']  # 计算平均订单金额
    category_stats = category_stats.sort_values('销售额', ascending=False)  # 按销售额排序
    
    # 显示商品大类统计
    print("产品类型统计:")
        print(category_stats)  # 输出统计量数据
    
    # 可视化商品大类销售额
    plt.figure(figsize=(14, 8))
    sns.barplot(x='产品类型', y='销售额', data=category_stats, palette='viridis')  # 绘制分类柱状图
    plt.title('各商品大类销售额')  # 设置图表标题
    plt.xlabel('产品类型')  # 设置X轴标签
    plt.ylabel('销售额(元)')  # 设置Y轴标签
    plt.grid(axis='y')  # 显示网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("商品大类销售额.png")  # 保存图表
    
    # 按商品子类统计销售数据
    subcategory_stats = data.groupby(['category', 'product_name']).agg({'order_id':'count','sales_amount':'sum'}).reset_index()  
    subcategory_stats.columns = ['产品类型', '产品子类', '订单数', '销售额']  # 重命名列
    subcategory_stats['平均订单金额'] = subcategory_stats['销售额'] / subcategory_stats['订单数']  # 计算平均订单金额
    subcategory_stats = subcategory_stats.sort_values('销售额', ascending=False)  # 按销售额排序
    
    # 显示Top3商品子类
    print("产品子类统计:")
    print(subcategory_stats.head(3))  # 输出前几行数据
    
    # 可视化Top3商品子类销售额
    top_subcategories = subcategory_stats.head(3)
    plt.figure(figsize=(16, 10))  # 创建图形画布
    sns.barplot(x='销售额', y='产品子类', hue='产品类型', data=top_subcategories, palette='viridis')  # 绘制分类柱状图
    plt.title('各产品子类销售额Top 3')  # 设置图表标题
    plt.xlabel('销售额(元)')  # 设置X轴标签
    plt.ylabel('产品子类')  # 设置Y轴标签
    plt.grid(axis='x')  # 显示网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("Top3子类销售额.png")  # 保存图表
    
    # 商品定价分析
    data['价格段'] = pd.cut(data['price'], bins=[0, 100, 200, 500], labels=['0-100元', '100-200元', '200-500元'])  #【要求2】
    
    # 按价格段统计销售数据
    price_range_stats = data.groupby('价格段').agg({'order_id':'count','sales_amount':'sum'}).reset_index()
    price_range_stats.columns = ['价格段', '订单数', '销售额']  # 重命名列
    price_range_stats['订单占比'] = price_range_stats['订单数'] / price_range_stats['订单数'].sum() * 100  # 计算订单占比
    price_range_stats['销售额占比'] = price_range_stats['销售额'] / price_range_stats['销售额'].sum() * 100  # 计算销售额占比
    # 显示价格段统计
    print("价格段统计:")
        print(price_range_stats)  # 输出价格数据
    
    # 可视化价格段销售情况
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 14))
    
    # 订单数占比柱状图
    sns.barplot(x='价格段', y='订单占比', data=price_range_stats, palette='Blues_d', ax=ax1)
    ax1.set_title('各价格段订单数占比')  # 设置图表标题
    ax1.set_xlabel('价格段')  # 设置X轴标签
    ax1.set_ylabel('订单数占比(%)')  # 设置Y轴标签
    ax1.grid(axis='y')  # 设置网格线
    
    # 销售额占比柱状图
    sns.barplot(x='价格段', y='销售额占比', data=price_range_stats, palette='Reds_d', ax=ax2)
    ax2.set_title('各价格段销售额占比')  # 设置图表标题
    ax2.set_xlabel('价格段')  # 设置X轴标签
    ax2.set_ylabel('销售额占比(%)')  # 设置Y轴标签
    ax2.grid(axis='y')  # 设置网格线
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("价格段销售占比.png")  # 保存图表
    
    # 商品销售分布树形图
    plt.figure(figsize=(16, 12))
    category_sales = category_stats[['产品类型', '销售额']].copy()  # 创建数据副本
    category_sales['标签'] = category_sales['产品类型'] + '\n' + category_sales['销售额'].apply(lambda x: f'{x:,.0f}元')  # 生成标签
    squarify.plot(sizes=category_sales['销售额'], label=category_sales['标签'], alpha=0.8, color=sns.color_palette("viridis", len(category_sales)))  # 绘制树形图
        plt.axis('off')  # 隐藏坐标轴
    plt.title('各产品类型销售额树形图', fontsize=18)  # 设置图表标题
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("产品销售额树形图.png")  # 保存图表
    
    # 商品季节性分析
    seasonal_product = data.groupby(['order_season', 'category'])['sales_amount'].sum().reset_index()  # 按季节和产品类型统计销售额
    season_pivot = seasonal_product.pivot(index='category', columns='order_season', values='sales_amount')  # 【要求3】
    season_pct = season_pivot.div(season_pivot.sum(axis=1), axis=0) * 100  # 计算各季节占比
    
    # 显示季节性销售占比
    print("季节商品销售占比(%):")
        print(season_pct)  # 输出百分比数据
    
    # 可视化季节性销售占比
    plt.figure(figsize=(14, 10))
    sns.heatmap(season_pct, annot=True, fmt='.1f', cmap='YlGnBu')  # 绘制热力图
    plt.title('各产品类型在不同季节的销售占比(%)')  # 设置图表标题
    plt.xlabel('季节')  # 设置X轴标签
    plt.ylabel('产品类型')  # 设置Y轴标签
    plt.tight_layout()  # 自动调整布局防止重叠
    plt.savefig("产品季节销售占比.png")  # 保存图表
    print("产品分析完成")  # 输出产品分析完成

# 进行产品分析
product_analysis(data)

平台代码与本地练习的区别

  • category_statsprice_range_statsplt.axisseason_pct 的打印/绘图行存在意外缩进,标准 Python 会在解析阶段停止。
  • product_analysis(orders) 内部还引用全局 data,使函数输入要求失效。保护块只能按平台要求原文写出,不能作为本地运行依据。
  • 参考实现应始终使用传入的 orders,先检查字段,再返回便于核对的品类与价格段统计。
展开保护块之外的参考实现
import pandas as pd  # 导入表格工具完成分组与价格分箱
def summarize_product_structure(orders):  # 定义显式输入的产品结构接口
    required = {'category', 'order_id', 'sales_amount', 'price'}  # 声明最小字段要求
    if not required <= set(orders.columns):  # 检查字段是否齐全
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时停止
    clean_orders = orders.dropna(subset=list(required)).copy()  # 删除关键字段缺失行
    if clean_orders.empty:  # 检查是否仍有可用记录
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空样本时停止
    category_stats = clean_orders.groupby('category', as_index=False).agg(订单数=('order_id', 'count'), 销售额=('sales_amount', 'sum'))  # 汇总品类订单与销售额
    category_stats['平均订单金额'] = category_stats['销售额'] / category_stats['订单数']  # 计算带分母的客单额
    clean_orders['价格段'] = pd.cut(clean_orders['price'], bins=[0, 100, 200, 500], labels=['0-100元', '100-200元', '200-500元'])  # 按平台练习说明分箱
    price_stats = clean_orders.groupby('价格段', observed=True, as_index=False).agg(订单数=('order_id', 'count'), 销售额=('sales_amount', 'sum'))  # 汇总价格段结果
    return category_stats, price_stats  # 返回两张可核对表

任务复盘|平台任务解答代码

运行后核对:核对 datacategory_mappingseasonscategory_stats 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:只改变一个关键输入或业务场景,先预测输出如何变化,再运行验证并解释变化原因。

品类销售额柱状图(独立实现)

展开课堂品类柱状图代码
category_totals = pd.Series({'茶叶': (class_sales * tea_share).sum(), '其他品类': (class_sales * (1 - tea_share)).sum()})  # 按品类合计课堂序列
figure, axis = plt.subplots(figsize=(7.0, 3.2))  # 创建适配幻灯片的画布
bars = axis.bar(category_totals.index, category_totals.values, color=['#00A4E6', '#007A86'], alpha=0.85)  # 绘制品类柱状图
for bar_handle in bars:  # 逐柱添加数值标签
    axis.text(bar_handle.get_x() + bar_handle.get_width() / 2, bar_handle.get_height(), f'{bar_handle.get_height():.0f}', ha='center', va='bottom', fontsize=16)  # 柱顶标注合计值
axis.set_ylabel('销售额合计(课堂单位)')  # 设置纵轴标签
axis.grid(True, axis='y', alpha=0.3)  # 显示横向浅网格
figure.tight_layout()  # 收紧边距
plt.show()  # 显示图形
图中展示课堂两品类合计柱状图(确定性非市场观测);读者应依据坐标、图例与注释比较主要模式。
Figure 5: 课堂两品类合计柱状图(确定性非市场观测)
  • 真实表按 category_mapping 映射后按品类 sum(),标签可换算万元

分析结论

下表是课堂判读模板,不是福建或真实市场结论;须替换为真实订单表复算,并保留时间留出与基线判断条件。

分析维度 判断条件通过后方可采用的判读
销售趋势 若真实数据复算后仍有周期性,再描述其幅度与稳定性
核心品类 若品类聚合结果领先,再报告核心品类及其销售额占比
移动平均 7日均线仅用于平滑短期波动,不替代回测
价格结构 若各价格段差异经核对稳定,再讨论定价含义
季节效应 若跨期验证仍成立,再设计差异化运营试点

业务建议

  • 统一前提:仅当真实订单表上的模型优于同目标朴素基线,且误差与误差带均满足预声明边界,才进入小规模试点;否则暂停。
  • 库存/营销:判断条件通过后按误差带上界设置试点容量,并设置止损;判断条件未通过不得据此备货或投放。
  • 价格/组合:品类与价格段结果须在独立时间窗核对后再试点,不把课堂序列结论外推到福建或真实农产品市场。

随堂练习

  • 问题 1|需要准备哪些数据?:清洗后的农产品销售日期、品类和销售额。
  • 问题 2|需要完成哪些操作?:运行 lst-ch29-sales-trend-prepare 至验证过程,构造趋势、产品结构和时间留出预测。
  • 问题 3|应得到哪些结果?:日/月/季度趋势、品类贡献、基线与预测误差表。
  • 问题 4|怎样确认结果可靠?:核对聚合总额与时间切分;样本不足或候选不优于基线时 暂不采用模型。
  • 问题 5|换一个情境,怎样继续应用?:把趋势与结构分析方法应用到另一中国农产品市场。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:农产品交易只使用平台提供的受保护表;适配受保护代码实际使用的 order_id/order_date/category/quantity/sales_amount
  • 平台期间要求:预测比较至少需要 18 个连续且销售额非零的月份。样本不足时,只做月度趋势和品类结构描述,不评价预测方法在新数据上的表现。
  • 平台练习答案|描述性回退(平台保护块之外):

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import pandas as pd  # 导入表格库
def describe_platform_agriculture(agri_transactions):  # 定义不声称预测验证的描述接口
    required={'order_id','order_date','category','quantity','sales_amount'}  # 对齐平台实际字段
    if not required<=set(agri_transactions.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时暂停分析
    transactions=agri_transactions.dropna(subset=list(required)).drop_duplicates().copy()  # 清除关键缺失与完全重复行
    if transactions.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空样本时停止
    transactions['order_date']=pd.to_datetime(transactions['order_date'],errors='coerce')  # 统一交易日期
    if transactions['order_date'].isna().any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无效日期停止
    monthly=transactions.set_index('order_date').resample('ME').agg(orders=('order_id','nunique'),sales=('sales_amount','sum'))  # 生成完整月度序列
    category=transactions.groupby('category').agg(orders=('order_id','nunique'),quantity=('quantity','sum'),sales=('sales_amount','sum'))  # 生成品类结构与分母
    complete=not monthly[['orders','sales']].eq(0).any().any()  # 检查连续月份是否均有非零观测
    status={'mode':'DESCRIPTIVE_ONLY','months':len(monthly),'complete_nonzero_months':complete,'forecast_ready':len(monthly)>=18 and complete}  # 明示回退边界
    if abs(category['sales'].sum()-transactions['sales_amount'].sum())>=1e-8: raise AssertionError('sales reconciliation failed')  # 核对品类销售额守恒
    return transactions,monthly,category,status  # 返回描述结果而不声称预测验证

教师参考解答|答案与说明 2

  • 平台练习答案|满足期间要求后的预测判断

教师参考解答|代码 2

展开代码(代码区可独立滚动)
from sklearn.linear_model import LinearRegression  # 导入趋势回归模型
def forecast_platform_agriculture(agri_transactions):  # 定义平台交易完整预测接口
    transactions,monthly,category,status=describe_platform_agriculture(agri_transactions)  # 先生成诚实描述回退
    if not status['forecast_ready']: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 预测期不足时停止
    monthly['seasonal_baseline']=monthly['sales'].shift(12)  # 构造上年同月基线
    monthly['trend_index']=range(len(monthly))  # 构造时间趋势特征
    model_frame=monthly.dropna(subset=['seasonal_baseline']).copy()  # 固定可比较样本
    validation_size=min(12,max(2,len(model_frame)//3))  # 事先设定末段验证长度
    if len(model_frame)-validation_size<4: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 明确训练窗下限
    training=model_frame.iloc[:-validation_size]  # 按时间切分训练窗
    validation=model_frame.iloc[-validation_size:].copy()  # 隔离同一验证窗
    trend_model=LinearRegression().fit(training[['trend_index']],training['sales'])  # 仅用训练窗拟合趋势
    validation['trend_forecast']=trend_model.predict(validation[['trend_index']])  # 生成验证期预测
    validation['baseline_ape']=(validation['sales']-validation['seasonal_baseline']).abs()/validation['sales'].abs()  # 计算基线逐期误差
    validation['trend_ape']=(validation['sales']-validation['trend_forecast']).abs()/validation['sales'].abs()  # 计算趋势逐期误差
    decision={'threshold':0.15,'threshold_status':'课堂误差情景,待业务规定批准','owner':'农产品运营负责人','action':'仅在趋势MAPE优于基线且过最低要求时小范围试用','recheck_when':'字段发生变化、缺月或MAPE超阈值','recheck_date':'下月第5个工作日'}  # 建立完整决策说明信息
    decision['status']='仅作课堂比较' if validation['trend_ape'].mean()<validation['baseline_ape'].mean() and validation['trend_ape'].mean()<=decision['threshold'] else '暂不采用'  # 同一时间段检查要求
    return monthly,category,validation,decision  # 返回趋势、结构、误差与检查要求

教师参考解答|答案与说明 3

  • 中国本地解答|长三角农业企业季度收入预测拓展应用

教师参考解答|代码 3

展开代码(代码区可独立滚动)
def select_latest_disclosure(frame,value_fields):  # 统一财报披露版本选择约定
    required={'ts_code','end_date',*value_fields}; versions=[c for c in ['ann_date','f_ann_date'] if c in frame.columns]  # 固定键与可接受版本字段
    if not required<=set(frame.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时停止
    work=frame.dropna(subset=list(required)).copy(); work['end_date']=pd.to_datetime(work['end_date'],errors='coerce')  # 清理必需字段
    for column in versions: work[column]=pd.to_datetime(work[column],errors='coerce')  # 解析披露日期
    keys=['ts_code','end_date']; work=work.dropna(subset=['end_date']); duplicate_mask=work.duplicated(keys,keep=False); before=int(duplicate_mask.sum())  # 统计选择前重复
    if before and (not versions or work.loc[duplicate_mask,versions].isna().all(axis=1).any()): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无版本依据时停止
    work['_version']=work[versions].max(axis=1) if versions else pd.NaT  # 合并双版本字段为排序键
    latest_version=work.groupby(keys)['_version'].transform('max') if versions else pd.Series(pd.NaT,index=work.index)  # 为每个公司报告期定位最晚披露时点
    latest_rows=work.loc[~duplicate_mask | work['_version'].eq(latest_version)].copy()  # 仅保留单例或并列最晚版本
    tied_mask=latest_rows.duplicated(keys,keep=False); tied_latest_rows=int(tied_mask.sum())  # 统计并列最晚版本行
    conflicting_latest_keys=int(latest_rows.loc[tied_mask].groupby(keys,dropna=False)[value_fields].nunique(dropna=False).gt(1).any(axis=1).sum()) if tied_latest_rows else 0  # 统计并列且数值冲突的披露键
    if conflicting_latest_keys: raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 并列最新值冲突时请先检查数据后再继续
    selected=latest_rows.drop_duplicates(keys+value_fields).drop_duplicates(keys).drop(columns='_version',errors='ignore')  # 仅合并数值完全一致的并列行
    after=int(selected.duplicated(keys).sum()); audit={'duplicate_rows_before':before,'tied_latest_rows':tied_latest_rows,'conflicting_latest_keys':conflicting_latest_keys,'duplicate_rows_after':after,'version_fields':versions}  # 返回并列与冲突检查
    if after: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 选择后仍重复则停止
    return selected,audit  # 返回唯一版本与检查依据

教师参考解答|代码 4

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具绑定规定资产
import pandas as pd  # 导入表格库以标准化HDF字段
import numpy as np  # 导入有限值与覆盖率检查工具
from sklearn.linear_model import LinearRegression  # 导入趋势回归
root=Path('/home/ubuntu/r2_data_mount/data/stock')  # 指定股票数据目录
basic_path,financial_path=root/'stock_basic_data.h5',root/'financial_statement.h5'  # 绑定可用HDF公司与财报快照
if not basic_path.exists() or not financial_path.exists(): raise FileNotFoundError('未找到课程股票数据文件,请从课程数据下载入口获取')  # 提示读者下载文件
basic=pd.read_hdf(basic_path,key='stock_basic_info',columns=['order_book_id','symbol','industry_name','province']).rename(columns={'order_book_id':'raw_code','symbol':'name','industry_name':'industry','province':'area'})  # 只读取公司筛选字段并标准化
basic['ts_code']=basic['raw_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一证券代码后缀
basic['area']=basic['area'].astype('string').str.replace(r'[省市]$','',regex=True)  # 统一省市简称
required_basic={'ts_code','name','industry','area'}  # 定义公司字段结构
required_financial={'ts_code','end_date','revenue','operate_profit'}  # 定义财报字段结构
focus=basic.loc[basic['industry'].str.contains('农|林|牧|渔',na=False) & basic['area'].isin(['上海','江苏','浙江','安徽'])]  # 筛选长三角农业企业
if focus.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空对象时停止
focus_raw_codes=focus['raw_code'].tolist()  # 保留HDF原始代码用于高效行筛选
financial=pd.read_hdf(financial_path,key='financial_data',where='order_book_id in focus_raw_codes',columns=['order_book_id','quarter','info_date','operating_revenue','profit_from_operation']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','info_date':'ann_date','operating_revenue':'revenue','profit_from_operation':'operate_profit'})  # 按对象读取并标准化财报字段
financial['ts_code']=financial['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一财报证券代码后缀
financial['end_date']=pd.PeriodIndex(financial['end_date'].str.upper(),freq='Q').end_time.normalize()  # 将季度键转换为报告期末日期
if not required_basic<=set(basic.columns) or not required_financial<=set(financial.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 标准化后仍缺列时停止
financial,version_audit=select_latest_disclosure(financial,['revenue','operate_profit'])  # 统一保留最晚披露版本

教师参考解答|代码 5

展开代码(代码区可独立滚动)
panel=financial.merge(focus[['ts_code','name','area']],on='ts_code',how='inner',validate='many_to_one').copy()  # 合并对象标签
if focus.empty or panel.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空对象或空连接时停止
panel=panel.sort_values(['ts_code','end_date'])  # 固定公司时间顺序
panel['fiscal_year']=panel['end_date'].dt.year  # 构造财政年键
panel['fiscal_quarter']=panel['end_date'].dt.quarter  # 构造财政季度键
if panel.duplicated(['ts_code','fiscal_year','fiscal_quarter']).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 公司期间键不唯一时停止
REVENUE_CADENCE='YTD_CUMULATIVE'  # 绑定 financial_statement 的利润表年初至今累计口径
if REVENUE_CADENCE!='YTD_CUMULATIVE': raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 先确认收入统计方式再继续
prior_cumulative=panel[['ts_code','fiscal_year','fiscal_quarter','revenue','operate_profit']].copy()  # 建立上一季度累计值查找表
prior_cumulative['fiscal_quarter']=prior_cumulative['fiscal_quarter']+1  # 只把同年上一季度键前移一季
prior_cumulative=prior_cumulative.rename(columns={'revenue':'prior_cumulative_revenue','operate_profit':'prior_cumulative_operate_profit'})  # 标明累计前值
panel=panel.merge(prior_cumulative,on=['ts_code','fiscal_year','fiscal_quarter'],how='left',validate='one_to_one')  # 按公司、财年、季度精确连接
needs_prior=panel['fiscal_quarter'].gt(1)  # 二至四季度累计值必须有同年上一季度
missing_prior=needs_prior & panel[['prior_cumulative_revenue','prior_cumulative_operate_profit']].isna().any(axis=1)  # 统计无法解累计值
panel['quarter_revenue']=np.where(panel['fiscal_quarter'].eq(1),panel['revenue'],panel['revenue']-panel['prior_cumulative_revenue'])  # 精确还原单季收入
panel['quarter_operate_profit']=np.where(panel['fiscal_quarter'].eq(1),panel['operate_profit'],panel['operate_profit']-panel['prior_cumulative_operate_profit'])  # 精确还原单季营业利润
panel['quarter_key']=panel['end_date'].dt.to_period('Q-DEC')  # 从报告期日期构造连续日历季度键
quarter_counts=panel.loc[~missing_prior].groupby('quarter_key')['ts_code'].nunique().sort_index()  # 统计可观察公司覆盖
eligible_quarters=quarter_counts.loc[quarter_counts.ge(2)]  # 至少两家公司才允许定义行业季度
if eligible_quarters.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无可分析季度时停止

教师参考解答|代码 6

展开代码(代码区可独立滚动)
coverage_floor=0.80  # 事先设定终点季度覆盖率下限
candidate_companies=panel.loc[~missing_prior,'ts_code'].nunique()  # 固定覆盖率分母
eligible_end=quarter_counts.loc[quarter_counts.div(candidate_companies).ge(coverage_floor)]  # 找到达到覆盖率的季度
if eligible_end.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 终点不稳定时停止
window_end=eligible_end.index.max(); window_quarters=pd.period_range(end=window_end,periods=16,freq='Q-DEC')  # 固定连续十六季度观察窗以支持四季样本外验证
presence=panel.loc[~missing_prior & panel['quarter_key'].isin(window_quarters)].groupby('ts_code')['quarter_key'].nunique()  # 统计公司在固定窗内的完整性
stable_codes=presence.loc[presence.eq(len(window_quarters))].index  # 稳定样本只保留十六季均可观察公司
if len(stable_codes)<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 样本组成不足时停止
panel=panel.loc[panel['ts_code'].isin(stable_codes) & panel['quarter_key'].isin(window_quarters)].copy()  # 固定公司组成与期间窗口
composition=panel.groupby('quarter_key')['ts_code'].nunique().reindex(window_quarters,fill_value=0)  # 检查每季公司组成
if not composition.eq(len(stable_codes)).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 组成变化请先检查数据后再继续
if panel[['quarter_revenue','quarter_operate_profit']].isna().any().any() or not np.isfinite(panel[['quarter_revenue','quarter_operate_profit']].to_numpy()).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 转换值检查要求
if panel['quarter_revenue'].eq(0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 单季分母检查要求
company_prior=panel[['ts_code','fiscal_year','fiscal_quarter','quarter_revenue']].rename(columns={'quarter_revenue':'seasonal_baseline'}).copy()  # 建立公司上年同季单季基线
company_prior['fiscal_year']=company_prior['fiscal_year']+1  # 将历史年键前移一年以匹配当期
panel=panel.merge(company_prior,on=['ts_code','fiscal_year','fiscal_quarter'],how='left',validate='one_to_one')  # 精确连接同公司上年同季基线
panel['baseline_ape']=(panel['quarter_revenue']-panel['seasonal_baseline']).abs()/panel['quarter_revenue'].abs()  # 计算同一时间段的基准方法误差
panel['operating_margin']=panel['quarter_operate_profit']/panel['quarter_revenue']  # 计算单季营业利润率检查要求指标
cohort_audit={'revenue_cadence':REVENUE_CADENCE,'candidate_companies':int(candidate_companies),'coverage_floor':coverage_floor,'window_start':str(window_quarters.min()),'window_end':str(window_quarters.max()),'stable_companies':int(len(stable_codes)),'excluded_companies':int(candidate_companies-len(stable_codes)),'companies_each_quarter':composition.astype(int).to_dict()}  # 发布口径、覆盖与固定组成检查

教师参考解答|答案与说明 4

  • 中国本地解答|时间切分与双模型比较:沿用上方通过判断条件的 panel

教师参考解答|代码 7

展开代码(代码区可独立滚动)
quarterly=panel.groupby(['fiscal_year','fiscal_quarter']).agg(end_date=('end_date','max'),revenue=('quarter_revenue','sum'),companies=('ts_code','nunique')).reset_index()  # 按精确日历季度汇总固定样本的单季收入
if quarterly.empty or quarterly['revenue'].eq(0).any() or not np.isfinite(quarterly['revenue']).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 汇总分母检查要求
industry_prior=quarterly[['fiscal_year','fiscal_quarter','revenue']].rename(columns={'revenue':'baseline'}).copy()  # 建立行业上年同季基线查找表
industry_prior['fiscal_year']=industry_prior['fiscal_year']+1  # 将历史年键前移一年以匹配当期
quarterly=quarterly.merge(industry_prior,on=['fiscal_year','fiscal_quarter'],how='left',validate='one_to_one').sort_values(['fiscal_year','fiscal_quarter'])  # 按相同日历季度精确连接基线
quarterly['trend_index']=range(len(quarterly))  # 构造趋势特征
model_frame=quarterly.dropna(subset=['baseline']).copy()  # 固定双模型共同样本
if len(model_frame)<12: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 要求至少八季训练与四季验证
validation_size=max(4,len(model_frame)//3)  # 事先设定至少四季度末段验证窗
if len(model_frame)-validation_size<8: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 训练窗检查要求
training,validation=model_frame.iloc[:-validation_size],model_frame.iloc[-validation_size:].copy()  # 按时间切分
model=LinearRegression().fit(training[['trend_index']],training['revenue'])  # 仅在训练窗拟合
validation['trend_forecast']=model.predict(validation[['trend_index']])  # 生成样本外预测
validation['baseline_ape']=(validation['revenue']-validation['baseline']).abs()/validation['revenue'].abs()  # 计算基线误差
validation['trend_ape']=(validation['revenue']-validation['trend_forecast']).abs()/validation['revenue'].abs()  # 计算趋势误差
if not np.isfinite(validation[['baseline_ape','trend_ape']].to_numpy()).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 非有限误差停止
decision={'threshold':0.15,'threshold_source':'课堂误差情景;实际应用应结合企业预算确定','owner':'农业企业经营负责人','action':'趋势MAPE优于同一时期的季节基准且不超过设定值时,可小范围试用','recheck_when':'公司组成、字段、收入计算方法改变,或缺少季度、MAPE超过设定值','recheck_date':'下一季度财报披露后第5个工作日'}  # 记录比较结果和建议
decision['baseline_mape']=float(validation['baseline_ape'].mean()); decision['trend_mape']=float(validation['trend_ape'].mean())  # 发布同一验证窗双模型误差
decision['status']='仅作课堂比较' if decision['trend_mape']<decision['baseline_mape'] and decision['trend_mape']<=decision['threshold'] else '暂不采用'  # 仅允许小范围试用或暂停
structure=panel.groupby(['end_date','area']).agg(companies=('ts_code','nunique'),revenue=('quarter_revenue','sum'),median_margin=('operating_margin','median')).reset_index()  # 汇总固定样本的地区结构
print(version_audit,cohort_audit,validation,decision,structure)  # 输出版本、统计口径、样本组成、模型比较与产品结构结果

教师参考解答|答案与说明 5

  • 解释答案:趋势预测必须使用时间切分并与同一时间段季节基线比较,结构占比须说明订单数与销售额分母;15% 只是课堂误差情景,须由真实 业务规定 与预算批准后才能用于决策。
  • 拓展应用答案:平台数据恢复时在同一快照调用完整函数;中国路线改用季度营业收入与营业利润率,不声称得到农产品订单或毛利;用总额守恒和同一时间段误差核对。
  • 参考结果:平台练习包括月度趋势、品类结构和逐期预测误差;公开教材案例使用 16 个稳定季度(至少 8 季训练、4 季验证),先把累计值还原为单季值,再比较季节基准预测与趋势预测的 MAPE,并说明地区结构和单季营业利润率。所需财务数据可从课程数据下载入口取得。
  • 边界 / 局限:结构占比说明分母;趋势外推须样本外验证
  • 常见错误:声称臆造的 case 路径是规定资产;随机切分;只报销量不报利润判断条件